//
// Created by benxb on 2021/11/28.
//

#include <access/database.h>
#include <common/logger.h>
#include <common/ini_reader.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <filesystem>
#include <catelog/query.h>

namespace benxdb {

bool Database::CreateTable(const std::shared_ptr<Table> &table) {
    if (tables_.find(table->GetName()) != tables_.end()) {
        return false;
    }
    if (TableExist(table->GetName())) {
        LOG_ERROR("Create table error: table exist.");
        return false;
    }
    table->SaveMetaData();
    tables_.insert({table->GetName(), table});
    table_managers_.insert({table->GetName(), new DataTableManager(table)});
    return true;
}

bool Database::DeleteTuple(std::string table_name, Tuple tuple) {
    LOG_INFO("NOT implemented");
    return false;
}

bool Database::Query(QueryPhrase query, std::vector<Tuple> &res) {
    if (table_managers_.find(query.GetTableName()) == table_managers_.end()) {
        if (!LoadTable(query.GetTableName())) {
            LOG_ERROR("No table with name %s", query.GetTableName().c_str());
            return false;
        }
    }
    table_managers_[query.GetTableName()]->Query(query, res);
}

bool Database::AppendPointCloud(const std::string &map_name, const pcl::PointCloud<pcl::PointXYZ> &point_cloud) {
    if (table_managers_.find(map_name) == table_managers_.end()) {
        LOG_ERROR("No table with name %s", map_name.c_str());
        return false;
    }
    for (pcl::PointXYZ point : point_cloud) {
        AppendPoint(table_managers_[map_name], point);
    }
    return true;
}

bool Database::AppendPoint(DataTableManager *table_manager, pcl::PointXYZ point) {
    std::vector<Value> vals;
    vals.emplace_back(Value(TypeId::FLOAT, point.x));
    vals.emplace_back(Value(TypeId::FLOAT, point.y));
    vals.emplace_back(Value(TypeId::FLOAT, point.z));
    Tuple tuple(vals);
    table_manager->AppendTuple(tuple);
}

bool Database::AppendPoint(DataTableManager *table_manager, PointXYZI point) {
    std::vector<Value> vals;
    vals.emplace_back(Value(TypeId::FLOAT, point.x));
    vals.emplace_back(Value(TypeId::FLOAT, point.y));
    vals.emplace_back(Value(TypeId::FLOAT, point.z));
    vals.emplace_back(Value(TypeId::FLOAT, point.intensity));
    Tuple tuple(vals);
    table_manager->AppendTuple(tuple);
}

bool Database::AppendPointCloud(const std::string &table_name, PointCloud<PointXYZI> &point_cloud) {
    if (table_managers_.find(table_name) == table_managers_.end()) {
        if (!LoadTable(table_name)) {
            LOG_ERROR("No table with name %s", table_name.c_str());
            return false;
        }
    }
//    std::shared_ptr<std::vector<PointXYZI>> points = point_cloud.AllPoints();
//    AppendManyPoints(table_managers_[table_name], points);
    table_managers_[table_name]->AppendPointCloud<PointXYZI>(point_cloud);
    return true;
}

bool Database::LoadTable(const std::string &table_name) {
    if (!TableExist(table_name)) {
        return false;
    }
    // load table meta data_
    auto table = std::make_shared<Table>(Table(table_name));
    tables_.insert({table_name, table});
    table_managers_.insert({table->GetName(), new DataTableManager(table)});
    return true;
}

bool Database::TableExist(const std::string &table_name) {
    std::string meta_file = GlobalConfig::MetaFilePath(table_name);
    std::ifstream f(meta_file.c_str());
    return f.good();

//    struct stat info;
//    return stat( GlobalConfig::DatasetDir(table_name), &info ) == 0;
}

Database::~Database() {
    for (const auto &manager : table_managers_) {
        delete manager.second;
    }
}

void
Database::AppendManyPoints(DataTableManager *table_manager, const std::shared_ptr<std::vector<PointXYZI>> &points) {
    std::vector<Tuple> tuples(points->size());
    auto table = table_manager->GetTable();
    for (int i = 0; i < points->size(); i++) {
        Tuple &tuple = tuples[i];
        PointXYZI &point = points->at(i);
        tuple.AppendValue(table->GetColumn(0), Value(TypeId::FLOAT, point.x));
        tuple.AppendValue(table->GetColumn(1), Value(TypeId::FLOAT, point.y));
        tuple.AppendValue(table->GetColumn(2), Value(TypeId::FLOAT, point.z));
        tuple.AppendValue(table->GetColumn(3), Value(TypeId::FLOAT, point.intensity));
    }
    table_manager->AppendManyTuple(tuples);
}

bool Database::QueryPointCloud(PointCloudQuery& query, PointCloud<PointXYZ> &res) {
    if (table_managers_.find(query.GetDatasetName()) == table_managers_.end()) {
        if (!LoadTable(query.GetDatasetName())) {
            LOG_ERROR("No dataset with name %s", query.GetDatasetName().c_str());
            return false;
        }
    }
    table_managers_[query.GetDatasetName()]->QueryPointCloud(query, res);
}

bool Database::CreateDataset(const std::string& dataset_name, DatasetType dataset_type) {
    std::vector<benxdb::Column> columns;
    FillColumnsByType(dataset_type, columns);
    return CreateTable(std::make_shared<benxdb::Table>(dataset_name, columns));
}

void Database::FillColumnsByType(DatasetType type, std::vector<benxdb::Column>& columns) {
    columns.emplace_back("x", benxdb::FLOAT);
    columns.emplace_back("y", benxdb::FLOAT);
    columns.emplace_back("z", benxdb::FLOAT);
    switch (type) {
        case DATASET_XYZ_INTENSITY:
            columns.emplace_back("intensity", benxdb::FLOAT);
            break;
        case DATASET_XYZ_FRAME:
            columns.emplace_back("frame", benxdb::INTEGER);
            break;
        default:
            break;
    }
}

// load config
Database::Database() {
    GlobalConfig::Init("config.ini");
}

void Database::AllDatasets(std::vector<std::string>& dataset_names) {
    std::string path = GlobalConfig::DatasetDir("");
    for (const auto & entry : std::filesystem::directory_iterator(path))
        dataset_names.push_back(entry.path());
}

bool Database::AppendPointCloud(const std::string &table_name, PointCloud<PointSTXYZ> &point_cloud) {
    if (table_managers_.find(table_name) == table_managers_.end()) {
        if (!LoadTable(table_name)) {
            LOG_ERROR("No table with name %s", table_name.c_str());
            return false;
        }
    }

    table_managers_[table_name]->AppendPointCloud<PointSTXYZ>(point_cloud);
    return true;
}

bool Database::AppendPointCloud(const std::string &table_name, PointCloud<PointSTXYZC> &point_cloud) {
    if (table_managers_.find(table_name) == table_managers_.end()) {
        if (!LoadTable(table_name)) {
            LOG_ERROR("No table with name %s", table_name.c_str());
            return false;
        }
    }

    table_managers_[table_name]->AppendPointCloud<PointSTXYZC>(point_cloud);
    return true;
}

}